/**
 *
 *    Copyright (c) 2024 Project CHIP Authors
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

#import "MTRMetricsCollector.h"
#import "MTRLogging_Internal.h"
#import "MTRMetricKeys.h"
#import "MTRMetrics.h"
#import "MTRMetrics_Internal.h"
#import <MTRUnfairLock.h>
#include <controller/CommissioningDelegate.h>
#import <os/lock.h>
#include <os/signpost.h>
#include <platform/Darwin/Tracing.h>
#include <system/SystemClock.h>
#include <tracing/metric_event.h>
#include <tracing/registry.h>

/*
 * Set this to MTR_LOG_DEBUG(__VA_ARGS__) to enable logging noisy debug logging for metrics events processing
 */
#define MTR_METRICS_LOG_DEBUG(...)

using MetricEvent = chip::Tracing::MetricEvent;

@implementation MTRMetricData {
    chip::System::Clock::Microseconds64 _timePoint;
    MetricEvent::Type _type;
    os_signpost_id_t _signpostid;
}

- (instancetype)init
{
    // Default is to create data for instant event type.
    // The key can be anything since it is not really used in this context.
    MetricEvent event(MetricEvent::Type::kInstantEvent, "");
    return [self initWithMetricEvent:event];
}

- (instancetype)initWithMetricEvent:(const MetricEvent &)event
{
    if (!(self = [super init])) {
        return nil;
    }

    _type = event.type();

    using EventType = MetricEvent::Type;
    switch (_type) {
    // Capture timepoint for begin and end to calculate duration
    case EventType::kBeginEvent:
    case EventType::kEndEvent:
        _timePoint = chip::System::SystemClock().GetMonotonicMicroseconds64();
        break;
    case EventType::kInstantEvent:
        _timePoint = chip::System::Clock::Microseconds64(0);
        break;
    }

    using ValueType = MetricEvent::Value::Type;
    switch (event.ValueType()) {
    case ValueType::kInt32:
        _value = [NSNumber numberWithInteger:event.ValueInt32()];
        break;
    case ValueType::kUInt32:
        _value = [NSNumber numberWithUnsignedInteger:event.ValueUInt32()];
        break;
    case ValueType::kChipErrorCode:
        _errorCode = [NSNumber numberWithUnsignedInteger:event.ValueErrorCode()];
        break;
    case ValueType::kUndefined:
        break;
    }

    _signpostid = OS_SIGNPOST_ID_NULL;

    MTR_METRICS_LOG_DEBUG("Initializing metric event data %s, type: %d, with time point %llu", event.key(), _type, _timePoint.count());
    return self;
}

- (void)setDurationFromMetricData:(MTRMetricData *)fromData
{
    auto duration = _timePoint - fromData->_timePoint;
    _duration = [NSNumber numberWithDouble:double(duration.count()) / USEC_PER_SEC];

    MTR_METRICS_LOG_DEBUG("Calculating duration for Matter metric with type %d, from type %d, (%llu - %llu) = %llu us (%llu s)",
        _type, fromData->_type, _timePoint.count(), fromData->_timePoint.count(), duration.count(), [_duration unsignedLongLongValue]);
}

- (NSString *)description
{
    return [NSString stringWithFormat:@"<MTRMetricData: Type %d, Value = %@, Error Code = %@, Duration = %@ us>",
                     static_cast<int>(_type), self.value, self.errorCode, self.duration];
}

- (os_signpost_id_t)signpostID
{
    if (_signpostid == OS_SIGNPOST_ID_NULL) {
        _signpostid = os_signpost_id_generate(__DARWIN_MATTER_SIGNPOST_LOGGER());
    }
    return _signpostid;
}

@end

@interface MTRMetricsCollector ()

- (void)registerTracingBackend;

- (void)unregisterTracingBackend;

@end

void StartupMetricsCollection()
{
    if ([MTRMetricsCollector sharedInstance]) {
        MTR_LOG("Initialized metrics collection backend for Darwin");

        [[MTRMetricsCollector sharedInstance] registerTracingBackend];
    }
}

void ShutdownMetricsCollection()
{
    [[MTRMetricsCollector sharedInstance] unregisterTracingBackend];
}

@implementation MTRMetricsCollector {
    os_unfair_lock _lock;
    NSMutableDictionary<NSString *, MTRMetricData *> * _metricsDataCollection;
    chip::Tracing::signposts::DarwinTracingBackend _tracingBackend;
    BOOL _tracingBackendRegistered;
}

+ (instancetype)sharedInstance
{
    static MTRMetricsCollector * singleton = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // Initialize the singleton and register the event handler
        singleton = [[MTRMetricsCollector alloc] init];
        if (singleton) {
            singleton->_tracingBackend.SetMetricEventHandler(^(MetricEvent event) {
                if (singleton) {
                    [singleton handleMetricEvent:event];
                }
            });
        }
    });
    return singleton;
}

- (instancetype)init
{
    if (!(self = [super init])) {
        return nil;
    }
    _lock = OS_UNFAIR_LOCK_INIT;
    _metricsDataCollection = [NSMutableDictionary dictionary];
    _tracingBackendRegistered = FALSE;
    return self;
}

- (void)registerTracingBackend
{
    std::lock_guard lock(_lock);

    // Register only once
    if (!_tracingBackendRegistered) {
        chip::Tracing::Register(_tracingBackend);
        MTR_LOG("Registered tracing backend with the registry");
        _tracingBackendRegistered = TRUE;
    }
}

- (void)unregisterTracingBackend
{
    std::lock_guard lock(_lock);

    // Unregister only if registered before
    if (_tracingBackendRegistered) {
        chip::Tracing::Unregister(_tracingBackend);
        MTR_LOG("Unregistered tracing backend with the registry");
        _tracingBackendRegistered = FALSE;
    }
}

static inline NSString * suffixNameForMetricType(MetricEvent::Type type)
{
    switch (type) {
    case MetricEvent::Type::kBeginEvent:
        return @"_begin";
    case MetricEvent::Type::kEndEvent:
        return @"_end";
    case MetricEvent::Type::kInstantEvent:
        return @"_event";
    }
}

static inline NSString * suffixNameForMetric(const MetricEvent & event)
{
    return suffixNameForMetricType(event.type());
}

- (void)handleMetricEvent:(MetricEvent)event
{
    std::lock_guard lock(_lock);

    using ValueType = MetricEvent::Value::Type;
    switch (event.ValueType()) {
    case ValueType::kInt32:
        MTR_METRICS_LOG_DEBUG("Received metric event, key: %s, type: %d, value: %d", event.key(), static_cast<int>(event.type()), event.ValueInt32());
        break;
    case ValueType::kUInt32:
        MTR_METRICS_LOG_DEBUG("Received metric event, key: %s, type: %d, value: %u", event.key(), static_cast<int>(event.type()), event.ValueUInt32());
        break;
    case ValueType::kChipErrorCode:
        MTR_METRICS_LOG_DEBUG("Received metric event, key: %s, type: %d, error value: %u", event.key(), static_cast<int>(event.type()), event.ValueErrorCode());
        break;
    case ValueType::kUndefined:
        MTR_METRICS_LOG_DEBUG("Received metric event, key: %s, type: %d, value: nil", event.key(), static_cast<int>(event.type()));
        break;
    default:
        MTR_METRICS_LOG_DEBUG("Received metric event, key: %s, type: %d, unknown value", event.key(), static_cast<int>(event.type()));
        return;
    }

    // Create the new metric key based event type
    auto metricsKey = [NSString stringWithFormat:@"%s%@", event.key(), suffixNameForMetric(event)];
    MTRMetricData * data = [[MTRMetricData alloc] initWithMetricEvent:event];

    if (event.type() == MetricEvent::Type::kBeginEvent) {
        os_signpost_interval_begin(__DARWIN_MATTER_SIGNPOST_LOGGER(), [data signpostID], "MetricEvent", "%s", event.key());
    } else if (event.type() == MetricEvent::Type::kInstantEvent) {
        os_signpost_event_emit(__DARWIN_MATTER_SIGNPOST_LOGGER(), OS_SIGNPOST_ID_EXCLUSIVE, "MetricEvent", "%s", event.key());
    }

    // If End event, compute its duration using the Begin event
    if (event.type() == MetricEvent::Type::kEndEvent) {
        auto metricsBeginKey = [NSString stringWithFormat:@"%s%@", event.key(), suffixNameForMetricType(MetricEvent::Type::kBeginEvent)];
        MTRMetricData * beginMetric = _metricsDataCollection[metricsBeginKey];
        if (beginMetric) {
            [data setDurationFromMetricData:beginMetric];
            os_signpost_interval_end(__DARWIN_MATTER_SIGNPOST_LOGGER(), [beginMetric signpostID], "MetricEvent", "%s", event.key());
        } else {
            // Unbalanced end
            MTR_LOG_ERROR("Unable to find Begin event corresponding to Metric Event: %s", event.key());
        }
    }

    // Add to the collection only if it does not exist as yet or pick latest value for instant event
    if (![_metricsDataCollection valueForKey:metricsKey] || event.type() == MetricEvent::Type::kInstantEvent) {
        // If this is the commissioning staging event, skip the cleanup to track the last stage completed in case of error
        // For all other events, just capture the value
        if (strcmp(event.key(), chip::Tracing::kMetricDeviceCommissionerCommissionStage) != 0 || event.ValueUInt32() != chip::Controller::CommissioningStage::kCleanup) {
            [_metricsDataCollection setValue:data forKey:metricsKey];
        }
    }
}

- (MTRMetrics *)metricSnapshotForCommissioning:(BOOL)resetCollection
{
    std::lock_guard lock(_lock);

    NSMutableArray * keysToDelete = [NSMutableArray array];
    MTRMetrics * metrics = [[MTRMetrics alloc] initWithCapacity:[_metricsDataCollection count]];
    [_metricsDataCollection enumerateKeysAndObjectsUsingBlock:^(NSString * key, MTRMetricData * obj, BOOL * stop) {
        // Commissioning metric keys predate the encoding of a category, so we need to filter out
        // all keys that use the encoding scheme here.
        if (![key hasPrefix:@METRICS_KEY_PREFIX]) {
            [keysToDelete addObject:key];
            [metrics setMetricData:obj forKey:key];
        }
    }];

    // Clear curent stats, if specified
    if (resetCollection) {
        [_metricsDataCollection removeObjectsForKeys:keysToDelete];
    }
    return metrics;
}

- (MTRMetrics *)metricSnapshotForCategory:(NSString *)category removeMetrics:(BOOL)removeMetrics
{
    std::lock_guard lock(_lock);

    NSString * keyPrefix = [NSString stringWithFormat:@METRICS_KEY_PREFIX "%@__", category];
    NSMutableArray * keysToDelete = [NSMutableArray array];
    MTRMetrics * metrics = [[MTRMetrics alloc] initWithCapacity:[_metricsDataCollection count]];
    [_metricsDataCollection enumerateKeysAndObjectsUsingBlock:^(NSString * key, MTRMetricData * obj, BOOL * stop) {
        if ([key hasPrefix:keyPrefix]) {
            [keysToDelete addObject:key];
            [metrics setMetricData:obj forKey:key];
        }
    }];

    // Clear curent stats, if specified
    if (removeMetrics) {
        [_metricsDataCollection removeObjectsForKeys:keysToDelete];
    }
    return metrics;
}

- (void)resetMetrics
{
    std::lock_guard lock(_lock);
    [_metricsDataCollection removeAllObjects];
}

@end
